/**
 * @license
 * Copyright 2017 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import '@polymer/iron-input/iron-input';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-icon/gr-icon';
import '../gr-permission/gr-permission';
import {
  AccessPermissions,
  PermissionArray,
  PermissionArrayItem,
  toSortedPermissionsArray,
} from '../../../utils/access-util';
import {
  EditablePermissionInfo,
  EditableRepoAccessGroups,
  PermissionAccessSection,
} from '../gr-repo-access/gr-repo-access-interfaces';
import {
  CapabilityInfoMap,
  GitRef,
  LabelNameToLabelTypeInfoMap,
  RepoName,
} from '../../../types/common';
import {fire} from '../../../utils/event-util';
import {IronInputElement} from '@polymer/iron-input/iron-input';
import {fontStyles} from '../../../styles/gr-font-styles';
import {grFormStyles} from '../../../styles/gr-form-styles';
import {sharedStyles} from '../../../styles/shared-styles';
import {css, html, LitElement, PropertyValues} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
import {assertIsDefined, queryAndAssert} from '../../../utils/common-util';

const GLOBAL_NAME = 'GLOBAL_CAPABILITIES';

// The name that gets automatically input when a new reference is added.
const NEW_NAME = 'refs/heads/*';
const REFS_NAME = 'refs/';

@customElement('gr-access-section')
export class GrAccessSection extends LitElement {
  @query('#permissionSelect') private permissionSelect?: HTMLSelectElement;

  @property({type: String})
  repo?: RepoName;

  @property({type: Object})
  capabilities?: CapabilityInfoMap;

  @property({type: Object})
  section?: PermissionAccessSection;

  @property({type: Object})
  groups?: EditableRepoAccessGroups;

  @property({type: Object})
  labels?: LabelNameToLabelTypeInfoMap;

  @property({type: Boolean})
  editing = false;

  @property({type: Boolean})
  canUpload?: boolean;

  @property({type: Array})
  ownerOf?: GitRef[];

  // private but used in test
  @state() originalId?: GitRef;

  // private but used in test
  @state() editingRef = false;

  // private but used in test
  @state() deleted = false;

  // private but used in test
  @state() permissions?: PermissionArray<EditablePermissionInfo>;

  constructor() {
    super();
    this.addEventListener('access-saved', () => this.handleAccessSaved());
  }

  static override get styles() {
    return [
      grFormStyles,
      fontStyles,
      sharedStyles,
      css`
        :host {
          display: block;
          margin-bottom: var(--spacing-l);
        }
        fieldset {
          border: 1px solid var(--border-color);
        }
        .name {
          align-items: center;
          display: flex;
        }
        .header,
        #deletedContainer {
          align-items: center;
          background: var(--table-header-background-color);
          border-bottom: 1px dotted var(--border-color);
          display: flex;
          justify-content: space-between;
          min-height: 3em;
          padding: 0 var(--spacing-m);
        }
        #deletedContainer {
          border-bottom: 0;
        }
        .sectionContent {
          padding: var(--spacing-m);
        }
        #editBtn,
        .editing #editBtn.global,
        #deletedContainer,
        .deleted #mainContainer,
        #addPermission,
        #deleteBtn,
        .editingRef .name,
        .editRefInput {
          display: none;
        }
        .editing #editBtn,
        .editingRef .editRefInput {
          display: flex;
        }
        .deleted #deletedContainer {
          display: flex;
        }
        .editing #addPermission,
        #mainContainer,
        .editing #deleteBtn {
          display: block;
        }
        .editing #deleteBtn,
        #undoRemoveBtn {
          padding-right: var(--spacing-m);
        }
      `,
    ];
  }

  override render() {
    if (!this.section) return;
    return html`
      <fieldset
        id="section"
        class="gr-form-styles ${this.computeSectionClass()}"
      >
        <div id="mainContainer">
          <div class="header">
            <div class="name">
              <h3 class="heading-3">${this.computeSectionName()}</h3>
              <gr-button
                id="editBtn"
                link
                class=${this.section?.id === GLOBAL_NAME ? 'global' : ''}
                @click=${this.editReference}
              >
                <gr-icon id="icon" icon="edit" filled small></gr-icon>
              </gr-button>
            </div>
            <iron-input
              class="editRefInput"
              .bindValue=${this.section?.id}
              @input=${this.handleValueChange}
              @bind-value-changed=${this.handleIdBindValueChanged}
            >
              <input
                class="editRefInput"
                type="text"
                @input=${this.handleValueChange}
              />
            </iron-input>
            <gr-button link id="deleteBtn" @click=${this.handleRemoveReference}
              >Remove</gr-button
            >
          </div>
          <!-- end header -->
          <div class="sectionContent">
            ${this.permissions?.map((permission, index) =>
              this.renderPermission(permission, index)
            )}
            <div id="addPermission">
              Add permission:
              <select id="permissionSelect">
                ${this.computePermissions().map(item =>
                  this.renderPermissionOptions(item)
                )}
              </select>
              <gr-button link id="addBtn" @click=${this.handleAddPermission}
                >Add</gr-button
              >
            </div>
            <!-- end addPermission -->
          </div>
          <!-- end sectionContent -->
        </div>
        <!-- end mainContainer -->
        <div id="deletedContainer">
          <span>${this.computeSectionName()} was deleted</span>
          <gr-button link="" id="undoRemoveBtn" @click=${this._handleUndoRemove}
            >Undo</gr-button
          >
        </div>
        <!-- end deletedContainer -->
      </fieldset>
    `;
  }

  private renderPermission(
    permission: PermissionArrayItem<EditablePermissionInfo>,
    index: number
  ) {
    return html`
      <gr-permission
        .name=${this.computePermissionName(permission)}
        .permission=${permission}
        .labels=${this.labels}
        .section=${this.section?.id}
        .editing=${this.editing}
        .groups=${this.groups}
        .repo=${this.repo}
        @added-permission-removed=${() => {
          this.handleAddedPermissionRemoved(index);
        }}
        @permission-changed=${(
          e: ValueChangedEvent<PermissionArrayItem<EditablePermissionInfo>>
        ) => {
          this.handlePermissionChanged(e, index);
        }}
      >
      </gr-permission>
    `;
  }

  private renderPermissionOptions(item: {
    id: string;
    value: {name: string; id: string};
  }) {
    return html`<option value=${item.value.id}>${item.value.name}</option>`;
  }

  override willUpdate(changedProperties: PropertyValues) {
    if (changedProperties.has('section')) {
      this.updateSection();
    }
    if (changedProperties.has('editing')) {
      this.handleEditingChanged(changedProperties.get('editing') as boolean);
    }
  }

  // private but used in test
  updateSection() {
    this.permissions = toSortedPermissionsArray(
      this.section!.value.permissions
    );
    this.originalId = this.section!.id;
  }

  // private but used in test
  handleAccessSaved() {
    if (!this.section) return;
    // Set a new 'original' value to keep track of after the value has been
    // saved.
    this.updateSection();
  }

  // private but used in test
  handleValueChange() {
    if (!this.section) {
      return;
    }
    if (!this.section.value.added) {
      this.section.value.modified = this.section.id !== this.originalId;
      this.requestUpdate();
      // Allows overall access page to know a change has been made.
      // For a new section, this is not fired because new permissions and
      // rules have to be added in order to save, modifying the ref is not
      // enough.
      fire(this, 'access-modified', {});
    }
    this.section.value.updatedId = this.section.id;
    this.requestUpdate();
  }

  private handleEditingChanged(editingOld: boolean) {
    // Ignore when editing gets set initially.
    if (!editingOld) {
      return;
    }
    if (!this.section || !this.permissions) {
      return;
    }
    // Restore original values if no longer editing.
    if (!this.editing) {
      this.editingRef = false;
      this.deleted = false;
      delete this.section.value.deleted;
      // Restore section ref.
      this.section.id = this.originalId as GitRef;
      this.requestUpdate();
      fire(this, 'section-changed', {value: this.section});
      // Remove any unsaved but added permissions.
      this.permissions = this.permissions.filter(p => !p.value.added);
      for (const key of Object.keys(this.section.value.permissions)) {
        if (this.section.value.permissions[key].added) {
          delete this.section.value.permissions[key];
        }
      }
    }
  }

  // private but used in test
  computePermissions() {
    let allPermissions;
    const section = this.section;
    if (!section || !section.value) {
      return [];
    }
    if (section.id === GLOBAL_NAME) {
      allPermissions = toSortedPermissionsArray(this.capabilities);
    } else {
      const labelOptions = this.computeLabelOptions();
      allPermissions = labelOptions.concat(
        toSortedPermissionsArray(AccessPermissions)
      );
    }
    return allPermissions.filter(
      permission => !section.value.permissions[permission.id]
    );
  }

  private handleAddedPermissionRemoved(index: number) {
    if (!this.permissions) {
      return;
    }
    delete this.section?.value.permissions[this.permissions[index].id];
    this.permissions = this.permissions
      .slice(0, index)
      .concat(this.permissions.slice(index + 1, this.permissions.length));
  }

  computeLabelOptions() {
    const labelOptions = [];
    if (!this.labels) {
      return [];
    }
    for (const labelName of Object.keys(this.labels)) {
      labelOptions.push({
        id: 'label-' + labelName,
        value: {
          name: `Label ${labelName}`,
          id: 'label-' + labelName,
        },
      });
      labelOptions.push({
        id: 'labelAs-' + labelName,
        value: {
          name: `Label ${labelName} (On Behalf Of)`,
          id: 'labelAs-' + labelName,
        },
      });
      labelOptions.push({
        id: 'removeLabel-' + labelName,
        value: {
          name: `Remove Label ${labelName}`,
          id: 'removeLabel-' + labelName,
        },
      });
    }
    return labelOptions;
  }

  // private but used in test
  computePermissionName(
    permission: PermissionArrayItem<EditablePermissionInfo>
  ): string | undefined {
    if (this.section?.id === GLOBAL_NAME) {
      return this.capabilities?.[permission.id]?.name;
    } else if (AccessPermissions[permission.id]) {
      return AccessPermissions[permission.id]?.name;
    } else if (permission.value.label) {
      if (permission.id.startsWith('labelAs-')) {
        return `Label ${permission.value.label} (On Behalf Of)`;
      } else if (permission.id.startsWith('removeLabel-')) {
        return `Remove Label ${permission.value.label}`;
      } else {
        return `Label ${permission.value.label}`;
      }
    }
    return undefined;
  }

  // private but used in test
  computeSectionName() {
    let name = this.section?.id;
    // When a new section is created, it doesn't yet have a ref. Set into
    // edit mode so that the user can input one.
    if (!name) {
      this.editingRef = true;
      // Needed for the title value. This is the same default as GWT.
      name = NEW_NAME as GitRef;
      // Needed for the input field value.
      this.section!.id = name;
      fire(this, 'section-changed', {value: this.section!});
      this.requestUpdate();
    }
    if (name === GLOBAL_NAME) {
      return 'Global Capabilities';
    } else if (name.startsWith(REFS_NAME)) {
      return `Reference: ${name}`;
    }
    return name;
  }

  private handleRemoveReference() {
    if (!this.section) {
      return;
    }
    if (this.section.value.added) {
      fire(this, 'added-section-removed', {});
    }
    this.deleted = true;
    this.section.value.deleted = true;
    fire(this, 'access-modified', {});
  }

  _handleUndoRemove() {
    if (!this.section) {
      return;
    }
    this.deleted = false;
    delete this.section.value.deleted;
    this.requestUpdate();
  }

  editRefInput() {
    return queryAndAssert<IronInputElement>(this, 'iron-input.editRefInput');
  }

  editReference() {
    this.editingRef = true;
    this.editRefInput().focus();
  }

  private isEditEnabled() {
    return (
      this.canUpload ||
      (this.ownerOf && this.ownerOf.indexOf(this.section!.id) >= 0)
    );
  }

  // private but used in test
  computeSectionClass() {
    const classList = [];
    if (this.editing && this.section && this.isEditEnabled()) {
      classList.push('editing');
    }
    if (this.editingRef) {
      classList.push('editingRef');
    }
    if (this.deleted) {
      classList.push('deleted');
    }
    return classList.join(' ');
  }

  // private but used in test
  handleAddPermission() {
    assertIsDefined(this.permissionSelect, 'permissionSelect');
    const value = this.permissionSelect.value as GitRef;
    const permission: PermissionArrayItem<EditablePermissionInfo> = {
      id: value,
      value: {rules: {}, added: true},
    };

    // This is needed to update the 'label' property of the
    // 'label-<label-name>' permission.
    //
    // The value from the add permission dropdown will either be
    // label-<label-name> or labelAs-<labelName>.
    // But, the format of the API response is as such:
    // "permissions": {
    //  "label-Code-Review": {
    //    "label": "Code-Review",
    //    "rules": {...}
    //    }
    //  }
    // }
    // When we add a new item, we have to push the new permission in the same
    // format as the ones that have been returned by the API.
    if (value.startsWith('label')) {
      permission.value.label = value
        .replace('label-', '')
        .replace('labelAs-', '');
    }
    // Add to the end of the array (used in dom-repeat) and also to the
    // section object that is two way bound with its parent element.
    this.permissions!.push(permission);
    this.section!.value.permissions[permission.id] = permission.value;
    this.requestUpdate();
    fire(this, 'section-changed', {value: this.section!});
  }

  private handleIdBindValueChanged = (e: BindValueChangeEvent) => {
    this.section!.id = e.detail.value as GitRef;
    this.requestUpdate();
    fire(this, 'section-changed', {value: this.section!});
  };

  private handlePermissionChanged = (
    e: ValueChangedEvent<PermissionArrayItem<EditablePermissionInfo>>,
    index: number
  ) => {
    this.permissions![index] = e.detail.value;
    this.requestUpdate();
  };
}

declare global {
  interface HTMLElementEventMap {
    /** Fired when the section has been modified or removed. */
    'access-modified': CustomEvent<{}>;
    /** Fired when a section that was previously added was removed. */
    'added-section-removed': CustomEvent<{}>;
    'section-changed': ValueChangedEvent<PermissionAccessSection>;
  }
  interface HTMLElementTagNameMap {
    'gr-access-section': GrAccessSection;
  }
}
